// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
// http://www.phpied.com/3-ways-to-define-a-javascript-class/
// 2013-09-28 usp


var music = new function ( )
{
	// Library for creating fretboard, chord and scale diagrams with HTML tables.
	/// Data 
	this.doubleflats =		new Array ( "Bbb",	"Cbb",	"",		"Dbb",	"",		"Ebb",	"Fbb",	"",		"Gbb",	"",		"Abb",	""			);
	this.flats =					new Array ( "",		"Bb",	"Cb",	"",		"Db",		"",		"Eb",	"Fb",	"",		"Gb",	"",		"Ab"	);
	this.naturals =				new Array ( "A",		"",		"B",		"C",		"",		"D",		"",		"E",		"F",		"",		"G",		""			);
	this.sharps =				new Array ( "",		"A#",	"",		"B#",	"C#",	"",		"D#",	"",		"E#",	"F#",		"",		"G#"		);
	this.doublesharps =	new Array ( "G##",	"",		"A##",	"",		"B##",	"C##",	"",		"D##",	"",		"E##",	"F##",	""			);
		
	this.intervalName = function ( length, sharp4 ) 
	{
		// Finds the interval name from interval length in semitones.
		var names = new Array ( "R", "m2", "2", "m3", "3", "4", "b5", "5", "m6", "6", "m7", "7", "R", "b9", "9", "#9", "b11", "11", "#11", "5", "b13", "13", "#13", "7" );
		while ( length > names.length ) length -= 12;
		if 	( length == 6 && sharp4 == true ) return "#4";
		return names [ length ];
	}
	 
	this.noteFileName = function( octaveNumber, baseNoteName, baseNoteOffset ) 
	{
		// Calculates a generic file name for a fret note, based on the octave number and the note
		// of the open string, and the fret number (baseNoteOffset).
		var midiNoteOffset  = music.distanceBetween( "C", baseNoteName ) + baseNoteOffset;
		var midiNoteIndex = midiNoteOffset % 12;
		var midiOctaveNumber = octaveNumber + ( midiNoteOffset / 12 ) | 0;
		if ( midiNoteIndex < 10 ) midiNoteIndex = "0" + midiNoteIndex;
		if ( midiOctaveNumber < 10 ) midiOctaveNumber = "0" + midiOctaveNumber;
		return midiOctaveNumber.toString ( ) + "-" + midiNoteIndex.toString( );
	};
	
	this.parseIntegerList = function ( values ) 
	{
		// Converts a list of comma separated values into an array of integers.
		// Returns null or an array of integers;
		if ( values == null || values.length == 0 ) return null;
		var list = values.split ( "," );
		for ( var i = 0; i < list.length; i ++ ) if ( list[ i ].length == 0 ) list[ i ] = null; else list[ i ] = parseInt ( list[ i ] );
		return list;
	}

	this.createDiatonicScale = function ( scaleRootNote, scaleIntervals ) 
	{
		// Create the result note array.
		var notes = new Array ( scaleRootNote, "", "", "", "", "", "", "", "", "", "", "" );
		// Create notes from given scale intervals.
		for ( var i = 1; i < scaleIntervals.length; i ++ )
		{
			notes[ music.intervalLength( scaleIntervals[ i ] ) ] = music.noteFromInterval ( scaleRootNote, scaleIntervals[ i ] );
		}
		// Calculate missing notes from the standard interval list.
		var standardIntervals = new Array ( "1", "m2", "2", "m3", "3", "4", "b5", "5", "b6", "6", "b7", "7" );
		for ( var i = 0; i < notes.length; i ++ )
		{
			if ( notes[ i ] == "" ) notes[ i ] = music.noteFromInterval ( scaleRootNote, standardIntervals[ i ] );
		}
		return notes;
	}

	this.noteFromInterval = function ( rootNote, intervalName ) 
	{
		// Calculates the note name from a root note and an interval name.
		if ( intervalName.toLowerCase ( ) == "r" ) intervalName = "1";
		// Note that the interval number is 1 based...
		var intervalNumber = parseInt ( intervalName.replace ( new RegExp ( "[mMb#da]", "g" ), "" ));
		var noteNames = "ABCDEFG";
		var resultNote = noteNames.substr (( noteNames.indexOf ( rootNote.replace ( new RegExp ( "[b#]", "g" ), "" )) + intervalNumber - 1 ) % 7, 1 );
		var desiredIntervalLength = music.intervalLength( intervalName );
		var resultIntervalLength = music.distanceBetween( rootNote, resultNote );
		while ( resultIntervalLength < desiredIntervalLength ) { resultNote += "#"; resultIntervalLength += 1; }
		while ( resultIntervalLength > desiredIntervalLength ) { resultNote += "b"; resultIntervalLength -= 1; }
		return resultNote;
	}

	this.distanceBetween = function ( note1, note2 ) 
	{
		// Compute the interval length between two notes in semitone steps.
		//
		var note1accs = note1.substr( 1 );
		note1 = note1.substr( 0, 1 ).toUpperCase( );
		var note2accs = note2.substr( 1 );
		note2 = note2.substr( 0, 1 ).toUpperCase( );
		var scale = new Array ( "A", "", "B", "C", "", "D", "", "E", "F", "", "G", "" );
		var result = -1;
		// Find the index of the first note.
		for ( var i = 0; i < scale.length; i++ )
		{
			if ( scale [ i ] == note1 )
			{ 
				result = -i; // subtract
				break; 
			}
		}
		// Find the index of the second note.
		if ( i == 12 ) return -1;
		for ( var i = 0; i < scale.length; i++ )
		{
			if ( scale [ i ] == note2 )
			{ 
				result += i; // add
				if ( i < 0 ) i += scale.length;
				break; 
			}
		}
		if ( i == 12 ) return -1;
		// Subtract first note accidentals.
		for ( var i = 0; i < note1accs.length; i++ )
		{
			switch ( note1accs.substr ( i, 1 ))
			{ 
				case "b": result += 1; break;
				case "#": result -= 1; break;
				default: return -1;
			}
		}
		// Add second note accidentals.
		for ( var i = 0; i < note2accs.length; i++ )
		{
			switch ( note2accs.substr ( i, 1 ))
			{ 
				case "b": result -= 1; break;
				case "#": result += 1; break;
				default: return -1;
			}
		}
		// Map into scale range.
		while ( result < 0 ) result += scale.length;
		while( result >= scale.length ) result -= scale.length;
		// Done.
		return parseInt ( result );
	} 
	
	this.intervalLength = function ( name ) 
	{
		// Calculates the offset (distance) in halftone steps from the scale root from its name.
		var basename = name.match( new RegExp( "[0-9]+|R|r|O|o" ));
		if ( basename != null )
		{
			var names = new Array( "R", "1", "2", "3", "4", "5", "6", "7", "O", "9", "11", "13" );
			var values = new Array( "0", "0", "2", "4", "5", "7", "9", "11", "12", "14", "17", "21" );
			for ( var i = 0; i < names.length; i ++ ) if ( basename == names[ i ] ) break;
			if ( i < names.length ) 
			{ 
				var result = values[ i ];
				var accidentals = name.replace ( basename, "" );
				for ( var i = 0; i < accidentals.length; i ++ )
				{
					switch ( accidentals.substr( i, 1 ).toLowerCase ( ))
					{
						case "b":
						case "m":
							result -= 1;
							break;
						case "#":
						case "a":
							result += 1;
							break;
						case "d":
							if ( new RegExp( "R|1|4|5" ).test ( basename )) result -= 1; // perfect intervals
							else result -= 2; // other intervals
					}
				}
				return parseInt( result );
			}
		}	
		// Error exit
		alert ( "Unexpected interval name: " + name ); 
		return null;
	} 


	// + + + Fretboard Table + + +
	//
	this.fretboard = new function ( ) 
	{
		this.markers = new Array ( "O", "I", "", "III", "", "V", "", "VII", "", "IX", "", "", "XII", "XIII", "", "XV", "", "XVII", "", "XIX", "", "XXI", "", "", "XXIV" );
		// Temporary data for createTable function 
		this.table = null;
		this.scaleRootNote = null;
		this.scaleIntervalNames = null;
		this.chordIntervalNames = null;
		this.upperMarkerRow = null;
				
		this.createTable = function ( table ) 
		{
			// - - - Fretboard creation code starts here.
			// 
			// Check table object reference.
			if ( table == null || table.length == 0 ) return;
			this.table = document.getElementById( table );
			if ( this.table == null || this.table.tagName.toLowerCase() != "table" ) return;
			// Assign functions to table object.
			this.table.showFrets = this.showFrets;
			this.table.setFretfilter = this.setFretfilter;
			this.table.isFretfiltered = this.isFretfiltered;
			// Analyze and process attributes.
			if ( this.table.notes != null && this.table.notes.length > 0 ) this.table.notes = this.table.notes.split ( "," );
			this.checkScaleAttribute ( );
			this.createNotesFromScale ( );
			if ( this.table.notes == null || this.table.notes.length != 12 ) { alert ( "table.notes attribute not defined or unable to compute!" ); return; }
			this.checkFretRange ( );
			this.checkChordAttribute ( );
			if ( this.table.strings != null && this.table.strings.length > 0 ) this.table.strings = this.table.strings.split ( "," );
			else this.table.strings = new Array ( "E4", "B3", "G3", "D3", "A2", "E2" );
			this.setupFretfilters( );
			this.createMarkerRows ( );
			this.createStringSet ( );
			this.createRowHeader ( );
			this.createStringNumberCells ( );
			this.createStringNameCells ( );
			this.createFretCells ( );
			this.table.showFrets ( this.table.viewmode );
			// Cleanup.
			this.table = null;
			this.scaleRootNote = null;
			this.scaleIntervalNames = null;
			this.chordIntervalNames = null;
			this.upperMarkerRow = null;
		}
		
		this.checkScaleAttribute = function ( ) 
		{
			// Analzyes the table scale attribute and creates the scaleIntervalNames array.
			// Format: Scale root note : scale interval list.
			this.scaleIntervalNames = new Array ( );
			if ( this.table.scale != null && this.table.scale.length > 0 )
			{
				this.table.scale = this.table.scale.split( ":" );
				if ( this.table.scale.length == 2 ) 
				{
					// Specified format: scaleRootNote:scaleIntervalList
					this.scaleRootNote = this.table.scale[ 0 ];
					this.table.scale = this.table.scale[ 1 ].split ( "," );
				}
				else if ( this.table.scale.length == 1 )
				{
					// No scale root note was specified.
					this.table.scale = this.table.scale.split ( "," );
				}
				for ( var i = 0; i < 12; i ++ ) this.scaleIntervalNames[ i ] = "";
				for ( var i = 0; i < this.table.scale.length; i ++ ) this.scaleIntervalNames[ music.intervalLength ( this.table.scale[ i ] ) ] = this.table.scale[ i ];
			}
		}
		
		this.createNotesFromScale = function ( ) 
		{
			// Create note list from scale intervals.
			if ( this.table.notes == null || this.table.note.length == 0 )
			{
				if ( this.scaleRootNote == null ) { alert ( "Scale root note must be specified if no notes were given!" ); return; }
				if ( this.table.scale == null || this.table.scale.length == 0 ) { alert ( "Scale intervals must be specified if no notes were given!" ); return; }
				this.table.notes = music.createDiatonicScale ( this.scaleRootNote, this.table.scale );
			}
		}
		
		this.checkFretRange = function ( ) 
		{
			// Analyzes the fretrange attribute.
			this.table.fretrange = music.parseIntegerList ( this.table.fretrange );
			if ( this.table.fretrange == null || this.table.fretrange.length == 0 ) this.table.fretrange = new Array ( 0, 12 );
		}
		
		this.checkChordAttribute = function ( ) 
		{
			// Analyzes the table chord attribute.
			// Format: NoteName:intervalName[,intervalName]
			if ( this.table.chord != null && this.table.chord.length > 0 ) 
			{
				// Determine chord root note and interval names.
				this.table.chord = this.table.chord.split ( ":" );
				var chordRootNote = this.table.chord[ 0 ];
				this.table.chord = this.table.chord[ 1 ].split( "," );
				// Map chord note names to diatonic scale
				this.chordIntervalNames = new Array ( "", "", "", "", "", "", "", "", "", "", "", "" );
				var chordRootOffset = music.distanceBetween ( this.table.notes[ 0 ], chordRootNote );
				for ( var i = 0; i < this.table.chord.length; i ++ )
				{
					this.chordIntervalNames[ ( chordRootOffset + music.intervalLength ( this.table.chord[ i ] )) % 12 ] = this.table.chord[ i ];
				}
			}
		}
		
		this.setupFretfilters = function ( ) 
		{
			// Syntax of the table.fretfilters attribute: 
			//		fretfilters ::= fretfilter[;fretfilter]...
			//		fretfilter ::= active,fgcolor,bgcolor,bordercolor,fretnumber[,fretnumber]...
			// It is assumed that the number of fretnumber entries matches the number of strings.
			// 
			if ( this.table.fretfilters == null || this.table.fretfilters.length == 0 ) return;
			// Split fretfilters into fretfilter array.
			this.table.fretfilters = this.table.fretfilters.split ( ";" );
			// Split filterdefs into arrays.
			for ( var i = 0; i < this.table.fretfilters.length; i ++ )
			{
				// Create the filterdef array.
				this.table.fretfilters[ i ] = this.table.fretfilters[ i ].split( "," );
				// Type convert active flag.
				this.table.fretfilters [ i ][ 0 ] = this.table.fretfilters [ i ][ 0 ] != "0";
				// Convert fret numbers.
				for ( j = 4; j < this.table.fretfilters [ i ].length; j ++ )
				{
					var s = this.table.fretfilters [ i ][ j ].toLowerCase ( );
					switch ( s )
					{
					case "x":
					case "":
						this.table.fretfilters [ i ][ j ] = -1;
						break;
					default:
						this.table.fretfilters[ i ][ j ] = parseInt ( s );
						break;
					}
				}
			}
		}
		
		this.setFretfilter = function ( number, state ) 
		{	
			//	This will become a fretboard table member function.
			//	Activates or deactivates a fret filter.
			//	number: fretfilter index. state: 0=off 1=on
			//
			// Isolate the fretfilter and set its state, active or inactive.
			var fretfilter = this.fretfilters [ number ];
			if ( fretfilter == null ) return;
			fretfilter [ 0 ] = state;
			// Loop through the strings.
			for ( var i = 0; i < this.rows.length; i ++ )
			{
				var row = this.rows [ i ];
				if ( row.className.toLowerCase ( ) != "string" ) continue;
				// Find the index of the specified fret in the fretfilter.
				// Attention: string numbers start with 1, not 0.
				var fretindex = fretfilter [ 3 + row.stringNumber ];
				if ( fretindex < 0 ) continue;
				// Account for the nut cell.
				if ( row.cells [ row.fretCellOffset + 1 ].className == "nut" ) fretindex += 1;
				// Add the fret cell offset for the current string.
				fretindex += row.fretCellOffset;
				var fretcell = row.cells [ fretindex ];
				if ( fretcell == null ) continue;
				// Change cell appearance according to the fretfilter state.
				if ( state )   // apply
				{
					// Save the default colors.
					fretcell.defaultColors = new Array ( fretcell.style.color, fretcell.style.backgroundColor, fretcell.style.borderColor );
					// Set the new colors defined by the fretfilter.
					fretcell.style.color = fretfilter [ 1 ];
					fretcell.style.backgroundColor = fretfilter [ 2 ];
					fretcell.style.borderColor = fretfilter [ 3 ];
				}
				else if ( fretcell.defaultColors != null )  // restore default colors.
				{
					// Note that default colors can be restore only if they had been saved before.
					fretcell.style.color = fretcell.defaultColors [ 0 ];
					fretcell.style.backgroundColor = fretcell.defaultColors [ 1 ];
					fretcell.style.borderColor = fretcell.defaultColors [ 2 ];
				}
			}
		}
		
		this.activateFretfilter = function ( evt, filterNumber )
		{
			// Ascend the object hierarchy and search for a fretboard table object.
			if ( evt != null ) { evt.cancelBubble = true; evt.returnValue = false; }
			var node = music.fretboard.findTableElement ( evt.srcElement );
			if ( node == null || node.setFretfilter == null || node.fretfilters == null ) return;
			// Loop through the fretboard filters and apply defined attributes to fret cells.
			for ( var i = 0; i < node.fretfilters.length; i ++ ) node.setFretfilter ( i, i == filterNumber );
		}
		
		this.isFretfiltered = function ( stringnumber, fret ) 
		{			
			// Returns true if the specified fret is in one of the active fretfilters.
			// NOTE: stringnumber is 1 based. Keep this in mind if used as array index.
			if ( this.fretfilters == null || this.fretfilters.length == 0 ) return false;
			for ( var i = 0; i < this.fretfilters.length; i ++ )
			{
				var fretfilter = this.fretfilters [ i ];
				if ( ! fretfilter [ 0 ] ) continue;  // not active.
				if ( fretfilter [ stringnumber + 3 ] == fret ) return true;
			}
			return false;
		}
		
		this.createMarkerRows = function ( ) 
		{
			this.table.markers = music.parseIntegerList ( this.table.markers );
			// Create the upper marker row.
			this.upperMarkerRow = this.table.rows.length > 0 ? this.table.rows[ 0 ] : this.table.insertRow ( );
			this.upperMarkerRow.className = "marker";
		}
		
		this.createStringSet = function ( ) 
		{
			// Create the stringset rows.
			for ( var i = 0; i < this.table.strings.length; i ++ )
			{
				var row = this.table.insertRow ( );
				row.className = "string";
				row.stringNumber = i + 1;   // real world string numbers start with 1...
				row.stringName = this.table.strings[ i ];   // note and octave number
				row.octaveNumber = row.stringName.match( new RegExp( "[0-9]+" ))[ 0 ];   // defines the absolute pitch of the open string.
				row.noteName = row.stringName.replace( row.octaveNumber, "" );   // string pitch without octave number
				row.octaveNumber = parseInt ( row.octaveNumber ); 
			}
		}
		
		this.createRowHeader = function ( ) 
		{
			// Create the row header cell(s).
			if ( this.table.rowheader != null && this.table.rowheader.length > 0 )
			{
				// insert (unused) row header cell in marker row
				var cell = this.table.rows [ 0 ].insertBefore ( document.createElement ( "TH" ));
				cell.name = "rowheader";
				// replace newlines in rowheader
				this.table.rowheader = this.table.rowheader.replace( new RegExp( "\\\\n", "g" ), "<br/>" );
				// insert row header in first string row.
				cell = this.table.rows [ 1 ].insertBefore ( document.createElement ( "TH" ));
				cell.name = "rowheader";
				cell.rowSpan = this.table.strings.length;
				cell.innerHTML = this.table.rowheader;
				// Determine the length of the longest line.
				var lines = this.table.rowheader.split ( "<br/>" );
				var maxlength = 0;
				for ( var i = 0; i < lines.length; i++ ) if ( lines[ i ].length > maxlength ) maxlength = lines[ i ].length;
				// Set the column width style according to the maximum length.
				this.table.rows[ 0 ].cells[ 0 ].style.width = maxlength.toString( ) + "em";
			}
		}

		this.createStringNumberCells = function ( ) 
		{
			// Create the string number cells.
			if ( this.table.showStringNumbers == null || this.table.showStringNumbers == "1" || this.table.table.showStringNumbers.toLowerCase( ) == "yes"  )
			{
				// insert string number cell in marker row
				var cell = this.	table.rows [ 0 ].insertBefore ( document.createElement ( "TH" ));
				cell.style.width = "1em";
				// insert string number cell in string rows
				for ( var i = 1; i <= this.table.strings.length; i++ )
				{
					var cell = this.table.rows [ i ].insertBefore ( document.createElement ( "TH" ));
					cell.className = "stringnumber";
					cell.innerHTML = i.toString ( );	
				}
			}
		}
		
		this.createStringNameCells = function ( ) 
		{
			// Create the string name cells.
			if ( this.table.showStringNames == null || this.table.showStringNames == "1" || this.table.table.showStringNames.toLowerCase( ) == "yes"  )
			{
				// insert string name cell in marker row
				var markercell = this.table.rows [ 0 ].insertBefore ( document.createElement ( "TH" ));
				// insert string number cell in string rows
				var maxlength = 0;
				for ( var i = 1; i <= this.table.strings.length; i++ )
				{
					var cell = this.table.rows [ i ].insertBefore ( document.createElement ( "TH" ));
					cell.className = "stringname";
					cell.innerHTML = this.table.strings[ i - 1 ];
					if ( maxlength < cell.innerHTML.length ) maxlength = cell.innerHTML.length;
				}
				markercell.style.width = maxlength.toString( ) + "em";
			}
		}
		
		this.createFretCells = function ( ) 
		{
			// Create the fretboard cells.
			for ( var i = this.table.fretrange[ 0 ]; i <= this.table.fretrange[ 1 ]; i++ )
			{
				// Create a marker cell in the marker row(s), but leave it empty for now.
				var markercell =	this.table.rows [ 0 ].insertBefore ( document.createElement ( "TD" ));
				if ( this.table.marker == null || this.table.markers.contains( i ))
				{
					markercell.innerHTML = this.markers[ i ];
					markercell.className = "used";
				}
				// Create a fret cell in all string rows
				for ( var j = 1; j <= this.table.strings.length; j++ )
				{
					var stringrow = this.table.rows[ j ];
					var cell = stringrow.insertBefore ( document.createElement ( "TD" ));
					if 	( i == 0 ) stringrow.fretCellOffset = stringrow.cells.length - 1;
					// Fret cell number.
					cell.fretNumber = i;
					cell.noteFileName = music.noteFileName( stringrow.octaveNumber, stringrow.noteName, cell.fretNumber );
					cell.noteName = this.table.notes[ ( music.distanceBetween ( this.table.notes[ 0 ], stringrow.noteName ) + cell.fretNumber ) % 12 ];
					cell.scaleRootFlag = cell.noteName == this.table.notes[ 0 ];
					cell.scaleIntervalName = this.scaleIntervalNames[ music.distanceBetween ( this.table.notes[ 0 ], cell.noteName ) ];
					cell.chordIntervalName = this.chordIntervalNames[ music.distanceBetween ( this.table.notes[ 0 ], cell.noteName ) ];
					cell.chordNoteFlag = cell.chordIntervalName != "";
					cell.chordRootFlag = cell.chordIntervalName == this.table.chord[ 0 ];
					cell.innerHTML = "&nbsp;";
					// Register event handlers.
					cell.onmouseenter = music.fretboard.onMouseEnterFretCell;
					cell.onmouseleave = music.fretboard.onMouseLeaveFretCell;
				}
				// insert nut column
				if ( i == 0 )
				{
					// Store the index of the first fret cell.
					this.table.rows[ 0 ].fretCellOffset = this.table.rows[ 0 ].cells.length - 1;
					// create nut cell in marker row
					var markercell = this.table.rows [ 0 ].insertBefore ( document.createElement ( "TH" ));
					markercell.style.width = "3px";
					// create nut cell in the first (!) string row - but not in the others!
					var nutCell = this.table.rows [ 1 ].insertBefore ( document.createElement ( "TH" ));
					nutCell.className = "nut";
					nutCell.rowSpan = this.table.strings.length;
				}
			}
		}
		
		this.showFrets = function ( what ) 
		{
			// Fills the fretboard table cell text with the desired value.
			// This function will be added to a fretboard table object, so 'this' will reference the table object.
			// Guard for objects other than table.
			if ( this == null || this.tagName == null || this.tagName != "TABLE" ) return;
			// Loop through all fret cells of the table.
			for ( var i = 0; i < this.rows.length; i ++ )
			{
				var row = this.rows[ i ];
				if ( row.className != "string" ) continue;
				for ( var j = 0; j < row.cells.length; j ++ )
				{
					var cell = row.cells[ j ];
					if ( cell.tagName == "TD" ) 
					{
						cell.innerHTML = "&nbsp;";
						cell.className = "";
						switch ( what.toLowerCase ( ))
						{
							case "allnotes":
								// Shows all the notes on the fretboard. Chord notes are highlighted.
								cell.innerHTML = cell.noteName;
								// Find the class names depending on note attributes.
								if ( cell.chordRootFlag ) cell.className = "chordroot";
								else if ( cell.scaleRootFlag ) cell.className = "scaleroot";
								else if ( cell.chordNoteFlag ) cell.className = "chordnote";
								else if ( cell.scaleIntervalName != "" ) cell.className = "scalenote";
								break;
							case "scale":
								// Shows only scale notes.
								if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length != 0 ) cell.innerHTML = cell.noteName;
								// Find the class names depending on note attributes.
								if ( cell.scaleRootFlag ) cell.className = "scaleroot";
								break;
							case "scaleintervals":
								// Shows only scale interval names.
								if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length > 0 ) cell.innerHTML = cell.scaleIntervalName;
								// Find the class names depending on note attributes.
								if ( cell.scaleRootFlag ) cell.className = "scaleroot";
								break;
							case "chord":
								// Shows only cells specified in the fretfilter attribute, and marks them as chord notes.
								if ( cell.chordIntervalName != null && cell.chordIntervalName.length > 0 
								&& this.isFretfiltered ( row.stringNumber, cell.fretNumber ))
								{
									cell.innerHTML = cell.noteName;
									if ( cell.chordRootFlag ) cell.className = "chordroot";
									else cell.className = "chordnote";
								}
								break;
							case "chordnotes":
								// Shows chord notes on the entire fretboard. Frets that match the fretfilter are marked 
								// as chord notes.
								if ( cell.scaleIntervalName != null && cell.chordIntervalName.length > 0 )
								{
									cell.innerHTML = cell.noteName;	
									if ( this.isFretfiltered ( row.stringNumber, cell.fretNumber ))
									{
										if ( cell.chordRootFlag ) cell.className = "chordroot";
										else cell.className = "chordnote";
									}										
								}
								break;
							case "chordoverscale":
								// Shows chord and scale notes on the entire fretboard. Frets that match the fretfilters are marked 
								// as chord notes.
								if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length > 0 || cell.chordIntervalName != null && cell.chordIntervalName.length > 0 )
								{
									cell.innerHTML = cell.noteName;	
									if ( this.isFretfiltered ( row.stringNumber , cell.fretNumber ))
									{
										if ( cell.chordRootFlag ) cell.className = "chordroot";
										else cell.className = "chordnote";
									}										
								}
								break;
							case "chordintervals":
								if ( cell.scaleIntervalName != null && cell.chordIntervalName.length > 0 )
								{
									cell.innerHTML = cell.chordIntervalName;
									if ( this.isFretfiltered ( row.stringNumber, cell.fretNumber ))
									{
										if ( cell.chordRootFlag ) cell.className = "chordroot";
										else cell.className = "chordnote";
									}
								}
								break;
						}
					}
				}
			}
		}
		
		this.onShow = function ( evt, mode ) 
		{
			// Ascends the object hierarchy and searches for an object that implements the
			// showFrets method. This is usually the fretboard table. Useful for putting links
			// in a table cell or caption elements.
			if ( evt != null ) evt.returnValue = false;
			var node = music.fretboard.findTableElement ( evt.srcElement );
			if ( node != null && node.showFrets != null ) node.showFrets ( mode );
		}
		
		this.findTableElement = function ( node ) 
		{
			// Ascends in the hierarchy to find the fretboard table element.
			while ( node != null  )
			{
				if ( node.tagName == "DOCUMENT" ) return null;
				if ( node.tagName == "TABLE" && node.className == "fretboard" ) return node;
				node = node.parentNode; 
			}
			return null;
		}
		
		this.onMouseEnterFretCell = function ( ) 
		{
			var cell = this;
			if ( cell == null || cell.tagName != "TD" ) return;
			var table = music.fretboard.findTableElement ( cell.parentNode );
			if ( table == null || table.hover == null || table.hover.length == 0 ) return;
			switch ( table.hover.toLowerCase ( ))
			{
				case "note":
					cell.innerHTML = cell.noteName;
					break;				
				case "scaleinterval":
					cell.innerHTML = cell.scaleIntervalName;
					break;
				case "chordinterval":
					cell.innerHTML = cell.chordIntervalName;
					break;
			}
			if ( cell.innerHTML == "" ) cell.innerHTML = "&nbsp;";
		}
		
		this.onMouseLeaveFretCell = function ( cell ) 
		{
			// Restores the cell content when mouse leaves the cell. Parameter "cell" is optional, 
			// and will be null or undefined if called as event handler to the mouseLeave event 
			// of the cell. Cell content is determined by the table viewmode attribute.
			// NOTE: "this" does NOT reference the table object!
			if ( cell == null ) cell = this;
			if ( cell == null || cell.tagName != "TD" ) return;
			var row = cell.parentElement;
			if ( row == null || row.className != "string" ) return;
			var table = music.fretboard.findTableElement ( row.parentNode );
			if ( table == null || table.hover == null || table.hover.length == 0 ) return;
			cell.innerHTML = "&nbsp;";
			switch ( table.viewmode.toLowerCase ( ))
			{
				case "allnotes":
					cell.innerHTML = cell.noteName;
					break;
				case "scale":
					if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length != 0 ) cell.innerHTML = cell.noteName;
					break;				
				case "scaleintervals":
					if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length > 0 ) cell.innerHTML = cell.scaleIntervalName;
					break;
				case "chord":
					if ( cell.scaleIntervalName != null && cell.chordIntervalName.length != 0  && table.isFretfiltered ( row.stringNumber, cell.fretNumber )) cell.innerHTML = cell.noteName;
					break;
				case "chordnotes":
					if ( cell.scaleIntervalName != null && cell.chordIntervalName.length > 0 ) cell.innerHTML = cell.noteName;
					break;
				case "chordoverscale":
					if ( cell.scaleIntervalName != null && cell.scaleIntervalName.length > 0 || cell.chordIntervalName != null && cell.chordIntervalName.length > 0 ) cell.innerHTML = cell.noteName;	
					break;
				case "chordintervals":
					if ( cell.scaleIntervalName != null && cell.chordIntervalName.length > 0 ) cell.innerHTML = cell.chordIntervalName;
					break;
			}
		}
	}
	
	// + + + Chord Table + + +
	//
	this.chord = new function ( ) 
	{
		/// Temporary data for creation. 
		this.table = null;
		this.intervalRow;
		this.notesRow;
		this.indexRow;

		this.createIntervalList = function ( ) 
		{
			this.table.intervals = this.table.intervals.split ( ":" );
			if ( this.table.intervals.length != 2 ) { alert ( "No chord root note specified." ); return; }
			else this.table.chordRoot = this.table.intervals[ 0 ];
			this.table.intervals = this.table.intervals[ 1 ].split ( "," );
		}
		
		this.createNotesFromIntervals = function ( ) 
		{
			// Derive notes from intervals, but obey a user defined list of notes.
			if ( this.table.notes != null && this.table.notes.length > 0 ) this.table.notes = this.table.notes.split( "," );
			else 
			{
				this.table.notes = new Array ( );
				for ( var i = 0; i < this.table.intervals.length; i ++ ) this.table.notes[ i ] = music.noteFromInterval ( this.table.chordRoot, this.table.intervals[ i ] );
			}
		}
		
		this.createIntervalsFromNotes = function ( ) 
		{
			if ( this.table.notes == null || this.table.notes.lengh == 0 ) { alert ( "No notes defined." ); return; }
			this.table.notes = this.table.notes.split ( "," );
			this.table.intervals = new Array ( "R" );
			for ( var i = 1; i < this.table.notes.length; i ++ ) this.table.intervals[ i ] = music.intervalName ( music.distanceBetween ( this.table.notes[ 0 ], this.table.notes[ i ] ));
		}
		
		this.createIndexList = function ( ) 
		{
			// Creates the index list for the notes and interval entries.
			this.table.noteIndices = new Array ( "0" );
			for ( var i = 1; i < this.table.intervals.length; i ++ ) this.table.noteIndices[ i ] = music.intervalLength ( this.table.intervals[ i ] );
		}
		
		this.setupRowPointers = function ( ) 
		{
			// Build the rows
			for ( var i = 0; i < this.table.roworder.length; i++ )
			{
				// Pick the first predefined row if there is a predefined one.
				var row = i == 0 ? this.table.tBodies[ 0 ].rows [ 0 ] : null;
				// Otherwise create a new row.
				if ( row == null ) row = this.table.tBodies[ 0 ].insertRow( -1 );
				// Assign the row to the variabels
				switch ( this.table.roworder [ i ] )
				{
					case "interval":
						this.intervalRow = row;
						this.intervalRow.className = "interval";		
						break;
					case "index":
						this.indexRow = row;
						this.indexRow.className = "index";		
						break;
					case "note":
						this.notesRow = row;
						this.notesRow.className = "note";		
						break;
				}
			}
		}
		
		this.buildRows = function ( ) 
		{
			// Build the various rows.
			for ( var i = 0; i < this.table.notes.length; i++ )
			{
				if ( this.intervalRow != null )
				{
					var cell = this.intervalRow.insertCell ( -1 );
					cell.innerHTML = this.table.intervals[ i ];
				}
				if ( this.indexRow != null )
				{
					var cell = this.indexRow.insertCell( -1 );
					cell.innerHTML = this.table.noteIndices[ i ];
				}
				if ( this.notesRow!= null )
				{
					var cell = this.notesRow.insertCell ( -1 );
					cell.innerHTML = this.table.notes[ i ];
				}
			}
		}

		this.insertRowHeader = function ( ) 
		{
			// Create the row header cell if given.
			if ( this.table.rowheader != null && this.table.rowheader.length > 0 && this.table.tBodies[ 0 ].rows [ 0 ] != null )
			{
				// Insert a new cell at the beginning of the first row.
				var cell = document.createElement ( "TH" );
				this.table.tBodies[ 0 ].rows [ 0 ].insertBefore ( cell, this.table.tBodies[ 0 ].rows [ 0 ].cells[ 0 ] );
				cell.className = "rowheader";
				cell.innerHTML = this.table.rowheader;
				cell.rowSpan = this.table.tBodies[ 0 ].rows.length;
				if ( this.table.rhwidth != null ) cell.style.width = this.table.rhwidth;
				else
				{
					// Determine the length of the longest line.
					var lines = this.table.rowheader.split ( "<br/>" );
					var maxlength = 0;
					for ( var i = 0; i < lines.length; i++ ) if ( lines[ i ].length > maxlength ) maxlength = lines[ i ].length;
					// Set the width style according to the maximum length.
					cell.style.width = maxlength.toString( ) + "em";
				}
			}
		}		

		this.addRowDescriptors = function ( ) 
		{
			// Add the row descriptors if present.
			if ( this.table.descriptions != null )
			{
				for ( var i = 0; i < this.table.descriptions.length; i++ )
				{
					var cell = this.table.tBodies[ 0 ].rows [ i ].insertBefore ( document.createElement ( "TH" ));
					cell.className = "rowdescriptor";
					cell.innerHTML = this.table.descriptions[ i ];
					cell.style.width = this.table.descriptions[ i ].length.toString() + "em";
				}
			}
		}

		this.create = function ( tableId ) 
		{
			// Builds a chord diagram table.
			// Check object reference.
			if ( tableId == null || tableId.length == 0 ) return;
			this.table = document.getElementById( tableId );
			if ( this.table == null || this.table.tagName.toLowerCase() != "table" ) return;
			// Prepare the interval names.
			
			if ( this.table.intervals != null && this.table.intervals.length > 0 )
			{
				this.createIntervalList ( );
				this.createNotesFromIntervals ( );
			}
			else  this.createIntervalsFromNotes ( );
			// Check input valid.
			if ( this.table.notes == null || this.table.notes.length == 0 || this.table.intervals == null || this.table.intervals.length == 0 ) return;
			this.createIndexList ( );
			// Prepare row order.
			if ( this.table.roworder != null && this.table.roworder.length > 0 ) this.table.roworder = this.table.roworder.split ( "," );
			else this.table.roworder = new Array ( "interval", "note", "index" );
			if ( this.table.descriptions != null && this.table.descriptions.length > 0 ) this.table.descriptions = this.table.descriptions.split ( "," );
			if ( this.table.rowheader != null && this.table.rowheader.length > 0 ) this.table.rowheader = this.table.rowheader.replace( new RegExp( "\\\\n", "g" ), "<br/>" );
			this.setupRowPointers ( );
			this.buildRows ( );
			this.insertRowHeader ( );
			this.addRowDescriptors ( );
			// Cleanup.
			this.table = null;
			this.intervalRow = null;
			this.notesRow = null;
			this.indexRow = null;
		}
	}
	
	// + + + Scale Table + + +
	//
	this.scale = new function ( ) 
	{
		// Creates a scale diagram table.
		function create ( table ) 
		{
			// Builds a scale diagram table.
			// Check object reference.
			if ( table == null || table.length == 0 ) return;
			table = document.getElementById( table );
			if ( table == null || table.tagName.toLowerCase() != "table" ) return;
			// Prepare the notes.
			table.notes = table.notes.split( "," );
			// Prepare the degrees.
			if ( table.degrees != null && table.degrees != "" ) table.degrees = table.degrees.split( "," );
			else
			{
				table.degrees = new Array ( "I", "II", "II", "III", "III", "IV", "V", "V", "VI", "VI", "VII", "VII", "I" );
				for ( var i in table.notes ) if ( table.notes [ i ] == "" ) table.degrees [ i ] = "";
			}
			// Transform to uppercase if requested.
			if ( ! table.uppercase )
			{
				for ( var i = 0; i < table.degrees.length; i++ )
				{
					if ( table.notes[ i ] != "" && table.notes[ ( i + 3 ) % 12 ] != "" ) table.degrees[ i ] = table.degrees[ i ].toLowerCase();
				}
			}
			// Prepare the intervals.
			if ( table.intervals != null && table.intervals != "" ) table.intervals = table.intervals.split ( "," );
			else
			{
				table.intervals = new Array ( "R", "m2", "2", "m3", "3", "4", "d5", "5", "m6", "6", "m7", "7", "R","b9", "9", "#9", "b11", "11", "#11", "", "b13", "13", "#13"  );
				for ( var i in table.notes ) if ( table.notes [ i ] == "" ) table.intervals [ i ] = "";
			}
			// Create the rows in the predefined order.
			if ( table.roworder == null || table.roworder == "" ) var roworder = new Array ( "degree", "interval", "index", "note" );
			else var roworder = table.roworder.split( "," );
			var degreeRow;
			var intervalRow;
			var indexRow;
			var notesRow;
			for ( var i in roworder )
			{
				// Pick the first predefined row if it is predefined.
				var row = i == 0 ? table.tBodies[ 0 ].rows [ 0 ] : null;
				// Otherwise create a new row.
				if ( row == null ) row = table.tBodies[ 0 ].insertRow( -1 );
				switch ( roworder [ i ] )
				{
					case "degree":		
						degreeRow = row;
						degreeRow.className = "degree";		
						break;
					case "interval":
						intervalRow = row;
						intervalRow.className = "interval";		
						break;
					case "index":
						indexRow = row;
						indexRow.className = "index";		
						break;
					case "note":
						notesRow = row;
						notesRow.className = "note";		
						break;
				}
			}
			// Build the various rows.
			for ( var i in table.notes )
			{
				if ( degreeRow != null )
				{
					var cell = degreeRow.insertCell ( -1 );
					cell.innerHTML = table.degrees[ i ] == null ? "&nbsp;" : table.degrees[ i ];					
				}
				if ( intervalRow != null )
				{
					var cell = intervalRow.insertCell ( -1 );
					cell.innerHTML = table.intervals[ i ] == null ? "&nbsp;" : table.intervals[ i ];
				}
				if ( indexRow != null )
				{
					var cell = indexRow.insertCell( -1 );
					cell.innerHTML = i;
					if ( table.notes[ i ] != "" ) cell.className = "used";
				}
				if ( notesRow!= null )
				{
					var cell = notesRow.insertCell ( -1 );
					cell.innerHTML = table.notes[ i ] == null ? "&nbsp;" : table.notes[ i ];
				}
			}
		} 
	}
}

Array.prototype.contains = function ( value )
{
	if ( value == null ) return;
	for ( var i = 0; i < this.length; i ++ ) if ( this[ i ] == value ) return true;
	return false
}
